For any suggestions or feedback regarding these notes,
please contact Pragy Agarwal
Its a "query" language - a way of specifying what to fetch from the DB.
SQL is not a DB type - it's just a query language.
SQL is the de-factor query language for Relational DBs.
When people say SQL, they mean Relational DB.
Even in Relational DBs, you can choose other querying languages - example: GraphQL.
Common Relational Database implementations: MySQL, PostgreSQL, OracleDB, MSSQL, SQLite, IBM DB2, Amazon RDS, ..
All these strengths apply at low scale
Low Scale: "queries/second, amount of data" is small enough to be handled by a single server.
Data is stored in tables - tables can have relations with each other (via foreign keys).
It is recommended to model data in a normalized manner.
Normalization prevents anomalies & reduces redundancy.
If a row violates the table's schema, then the DB will let you know. It won't let the write succeed.
Extremely powerful & good to have.
All or nothing: Each transaction is either executed completely (all rows) or not at all. There are no partial states.
Imagine that Khushboo is transferring 1 million $ to Vishudh.
It should not be the case that the money gets deducted from Khushboo, but isn’t credited to Nimish. We want to ensure that either the money is not deducted from Khushboo, or, if deducted, it must be successfully credited to Nimish.
CAP Consistency: no stale reads — when we have multiple copies of the data (replicas/cache), and some copy is out-of-sync with other copies, then we should not read from the stale copy.
ACID Consistency: db constraints are enforced
Multiple transactions that are running simultaneously don't mess with each other.
There's different isolation levels. (homework: read up on this)
Transaction 1: Khushboo– 1 million $ → Vishudh
Transaction 2: Khushboo – 1 million $ → Nandani
Khushboo started with 1 million $
T1: S 1..3
T2: S 1..3
T1: S 4 Khushboo’s new balance = 0
T2: S 4 Khushboo’s new balance = 0
T1: S 5..6
T2: S 5..6
This is lack of isolation! This should NOT happen.
Any transaction that has been executed will be stored in non-volatile storage (HDD/SSD) and not just the volatile RAM.
Note: Durability does NOT protect you against HDD failures - only replication does.
When the scale becomes large, all the strengths become weaknesses!
Large Scale: data or req/s is too large to fit on a single server
On the frontend, we (almost) always need to show “denormalized” data (data about a lot of entities)
Therefore, to display the data, we need to perform joins.
For example, to display the page (https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array) you will need to join around 30-40 tables
What if the data itself is inherently unstructured?
Amazon has 10 million+ products across 10,000+ categories.
Every category has a different attribute set
products: id, name, brand, price, Color, Fabric, Neck-shape, Sleeve length, Screen Size, RAM, CPU, GPU, OS, Page thickness, Number of Pages, Ruled, ...
Too many columns (10,000 categories, 10 unique cols per category ⇒ 100,000 columns)
products: id, name, brand, price
tshirts: product_id, Color, Fabric, Neck-shape, Sleeve length
laptops: product_id, Screen Size, RAM, CPU, GPU, OS
notebooks: product_id, Page thickness, Number of Pages, Ruled
… 10,000 such tables, one for each product type
products: id, name, brand, price
product_attributes: product_id, attribute_name, attribute_value
product_id | attribute_name | attribute_value |
1 | RAM | 16GB |
1 | CPU | i9 14400k |
1 | Screen Size | 17" |
1 | Fabric | cotton |
2 | Neck Type | Rounded |
2 | Sleeve Length | full |
2 | Fabric | cotton |
So we see that since the data was inherently "semi-structured" SQL was not a good choice.
At low scale (only 2-3 different product categories) SQL could've worked.
Yes, and no!
Relational DBs like Postgres, MySQL have 1st class support for JSON. However, it still is not designed to handle scale.
When the data is large (high scale), then you need sharding - because you can't store all the data in a single server.
Sharding nullifies ACID
SQL dbs provide ACID guarantees only within a single server.
Because it is easy to do that
If you've multiple servers
Providing ACID guarantees across shards is extremely difficult
No SQL =/= don't use SQL
NoSQL = Not Only SQL - we will still continue using relational databases, but, we will augment their capabilities with additional non-relational databases
SQL dbs have existed for a very long time - even before modern computers came into picture, the theoretical foundations for relational algebra were already laid.
NoSQL dbs are extremely recent
Your de-facto choice should ALWAYS be Relational (SQL) databases.
You can use NoSQL - but, only if, you can justify the need for it.
Modern SQL databases (like postgres, mysql) can do absolutely everything that any NoSQL can do & even more
SQL
NoSQL
https://aws.amazon.com/compare/the-difference-between-acid-and-base-database/
The system as a whole will remain available, even though some services might be unavailable for a small fraction of the users for some time.
High availability (for the entire system, not for individual services/users)
ACID transactions provide atomicity (all or nothing)
Soft state: Transactions can be in a partial state for some time (even for extended time – weeks!) - they're not all or nothing — but eventually, the data will become consistent and atomic.
(we will see this in the last class of HLD - distributed transactions in microservices - Saga Pattern)
There might be stale reads. But, eventually (if we wait long enough) every write will be reflected across the entire system (all replicas)
SQL databases require manual sharding - they don't provide built-in support for sharding
(note: most modern SQL dbs have built-in support for replication, but not sharding)
Note: you can shard a SQL database either
NoSQL db are automatically sharded - they're built with horizontal scaling in mind.
You just have to get a bunch of servers, and install the database on them - and they will figure stuff out themselves (LB, autoscaling, sharding, data distribution, replication, fault tolerance..)
SQL databases discourage denormalization & redundancy to remove anomalies.
NoSQL databases realise that
NoSQL dbs encourage storing data in a denormalized manner / semi-structured (sometimes even schemaless) manner.
SQL databases have tons of features
Any feature that you can think of, modern SQL dbs have it.
The only con of SQL is that it is feasible only at low scale.
SQL databases "generalize" - jack of all trades, master of none (they don’t work at scale)
NoSQL databases can work at massive scale because they don't support most of the features
NoSQL databases "specialize", jack of 1 trade, and master of it
Sharding key decides how your data gets distributed across various db servers.
It will also determine how the queries need to be routed to fetch the data.
Primary Key: Uniquely identifies a item in the data (e.g. row in a table) — what you’re talking about
Q: Does a Primary key have to be unique across tables?
No.
It is totally okay for there to be a user_id = 1 and a product_id = 1
Both can have the value 1, because we are querying different tables – we’re talking about different entities.
Q: Does a Primary key have to be unique across shards, for the same table?
Yes! Primary key should be unique for each row within a table, irrespective of whether the table is on a single server, or whether the table is sharded.
The following is wrong
Shard 1 has user_id = 1 name = Sai
Shard 2 has user_id = 1 name = Abhishek
Sharding Key: Just tells you how to distribute the data: what data goes to what server —- where to find the data
users (id, name, gender)
A, Akshay, Male
B, Balaji, Male
C, Chandani, Female
for user table, the PK = id, and SK = id
user_posts (id, user_id, title, content)
1, A, Hi, Hello World
2, A, Bye, Going to sleep
3, B, Wassup, What’s everyone doing
4, C, Context?, Who are you guys? Why are you in my home?
for user_posts table, the PK = id, SK = user_id
user_friendship (id, user_id, friend_id, affinity)
1, A, B, 100%
2, B, A, 100%
3, A, C, 50%
4, C, A, 50%
for user_friendship table, the PK = id, SK = user_id
Suppose we’re sharding by the user_id, then all tables must have user_id
Shard 1:
users (id, name, gender)
A, Akshay, Male
user_posts (id, user_id, title, content)
1, A, Hi, Hello World
2, A, Bye, Going to sleep
user_friendship (id, user_id, friend_id, affinity)
1, A, B, 100%
3, A, C, 50%
Shard 2:
users (id, name, gender)
B, Balaji, Male
user_posts (id, user_id, title, content)
3, B, Wassup, What’s everyone doing
user_friendship (id, user_id, friend_id, affinity)
2, B, A, 100%
Shard 3:
users (id, name, gender)
C, Chandani, Female
user_posts (id, user_id, title, content)
4, C, Context?, Who are you guys? Why are you in my home?
user_friendship (id, user_id, friend_id, affinity)
4, C, A, 50%
Q: Does the Sharding key have to be unique across tables?
No. This question is weird, because all the tables are sharded in the same way.
Q: Does the Sharding key have to be unique across shards?
Yes. Because if it was same, then we wouldn’t route to different shards.
Both!
Both of these keys will coexist.
It is common to use the same column as both the primary & sharding key
for example, we can use user_id as primary key for users table, and also shard the db by user_id
but not always. It is totally possible to have a different primary key and different sharding key
for the posts table, the primary key is post_id
You need to use both!
Sharding key will tell you which server to go to (routing).
Primary key will tell you which entry to touch inside that server.
It is possible to omit the Sharding Key. Just the PK is enough to uniquely identify a row. However, if the Sharding key is not used, then our query will be a fan-out query (we will have to hit all shards)
Suppose you’re going to school to find your little sister.
Primary Key: Your sister’s name & her student id
Sharding Key: The class number
To find your sister, you MUST know the primary key.
If you also know the Sharding key, then your search will be faster, because you will exactly which classroom (server) to go to.
If you don’t know the sharding key, then you will have to fan-out and search through all classrooms (servers)
No. The sharding key can be composite (have multiple columns), but, it MUST be the same group of columns across the entire database.
If you need different sharding keys, you need different databases.
If there are multiple tables in the DB, does that mean that ALL these tables must have this sharding key column?
Yes.
If you have some data that doesn’t need to be split, or is required across all shards, in that case, you can either
Yes. Totally allowed.
For example, you can choose (class-number, gender) as sharding key, like in a non-co-ed school.
Yes, possible. But, not recommended!
Changing the sharding key would require complete re-shuffling of the data across all the servers — very very expensive, and will most likely require database downtime.
This is why it is very important to choose a good sharding key upfront when you’re the designing the architecture.
Bad idea.
Still, the LB won’t care. It will just treat “null” as any another value.
So, all rows, which have sharding_key = null will end up in the same shard.
Suppose you’re sharding by column “SK”.
Consider 2 rows, R1 and R2.
All key values should be equally likely.
If some values are more likely than others - that will lead to "hot shards" (a shard that is overwhelmed with data/requests)
Age: ages 0-5 are less likely. Ages 50+ are less likely. Ages 15-30 are most likely. There will be no users above the age 100 (very unlikely).
Sharding based on age will be a bad idea, as it will lead to poor load distribution.
Gender: depending on the application, you can have hot shards. Most of the students @ Scaler are male (because there’s gender disparity in higher technical studies)
User id: user is unique for each user. Suppose you want to shard posts by the user id, then of course, there will be some users that have made many many posts and some users that have made 0 posts.
But since each server has 100,000s of users, overall, the data distribution across the servers will be pretty even — the normal users & the influences will get distributed more or less equally across the various servers.
Note: this is not always true: eg, "celebrity problem", especially for notifications systems.
No. A single server will house multiple users (because multiple values of the key can hash to the same server)
If Post_13 is by User_1 and Post_20 is by User_2, then most likely, these posts will end up in different servers.
Cardinality = count of the set of possible values
Age: {0, 123} has 124 possibilities max.
This means that if we have 125 servers, then 1 server will not get any value.
Basically, we're limited to max 124 servers. We cannot scale any further than that.
Gender: {Male, Female, LGBTQ+} has 3 possibilities max.
This means that if you have 4 servers, then 1 server will be idle.
Max scaling possible is only 3 servers.
Even if you have 1 billion users, you will be forced to somehow fit them into 3 shards.
User_Id: {64 bit value} has 16 quintillion possibilities
There's no limit to the number of servers we can scale to. 10 million servers? No issue.
Note: we're not saying that we will have 16 quintillion servers. A single server will house millions of users.
Cardinality of the sharding key doesn’t define how many servers you have. But it gives you an upper limit on how many servers you can scale to in the future.
Because if the sharding key is not part of the request, then how will you route the request in the first place? You will have to go to every shard - fan-out (bad)
Imagine that we’re sharding by gender. We want to load the profile page of Tanisq.
select * from user_profiles
where user_id = [1234]
This query will fan-out, because the DB LB doesn’t know which shard to hit, because it doesn’t know the gender of Tanisq.
select * from user_profiles
where user_id = [1234]
and gender = ‘MALE’
This query will not fan out. But do you think your app server will make this query?
When the app server is trying to render the profile page, how on earth does it get the value for the gender column?
Most frequent queries should have to hit only 1 (or at most 2) shards.
No frequent query should lead to a fan-out request.
Note: rare queries are okay to fan-out — you cannot optimize every single query
The value of the sharding key should not change for any row.
Because if we change the value of sharding key, then we will have to re-shuffle the data.
Age: what happens when the user's B'day comes? We will have to move the user to a different server because the age has changed.
A user can have accounts across multiple cities. If we shard by location, then, we will need to store the user's data across multiple shards.
(same issues as location — branch-id is just more granular)
ideal sharding key here.
This means that 1 user's data will be housed entirely within a single server (note: 1 server still houses thousands of users)
Any id/entity that is generated by the system (ticket / booking / transaction) cannot be used as the sharding key.
Q: isn’t the user_id also generate by the system.
Yes, but only once, when the user registers. For any subsequent request made by the user, the user_id is available at the client side, and the client will send this user id along with every request (via cookie or auth-token)
NEVER choose timestamp.
account_id is fine for most of our queries, but not for the basic user dashboard.
In the interface, the user might want to see the list of accounts they have.
If we shard by the account_id, then different accounts of the same user might go to different servers.
So the dashboard request for the user will be a fan-out query.
book_ticket(user_id, train_id, date_of_journey, class, seat_preference, meal_preference, ...) => ticket: {id, details}
ticket_id is not part of the request - it is generated in the response
Timestamp - so big no no
Any data for past dates will now be immutable. You cannot book a ticket for a journey that has already happened in the past. You cannot modify (cancel, add lunch services) the ticket if the journey has already completed.
Users are distributed across the shards.
Let’s suppose that Ashok is trying to book a ticket in Rajdhani
We need to ensure that Ashok doesn't get allotted the same seat.
If we shard by user_id, then Sanjana’s ticket data is in her shard, Rohit's ticket is in his shard.
To ensure Ashok doesn't get a duplicate seat, we've to hit both Sanjana & Rohit's shards. In fact we need to hit all the shards.
We don't know which users have booked which tickets.
We have to hit all shards to check if a particular seat has been booked or not - fan out.
If we shard by train_id, then tickets of 1 train are stored completely within a single shard.
can their seats collide? No, the trains are different!
So when checking for duplicates, we only need to check the tickets for this train - just 1 shard.
IRCTC facilitates ~13k passenger trains ⇒ cardinality is decent
Note that user data can be sharded by user_id (user dashboard service)
And for booking ticket, the ticket data can be sharded by train_id (booking microservice)
Each ticket will be stored in 2 database - tickets db (sharded by train_id), and the user's db (sharded by user_id)
That will be a fan-out.
But note that this is NOT a frequent query!
If it were a frequent query, then we would come up with another solution
This means that all messages that are (both) sent and received by Sanjana will be in 1 shard.
If Sanjana wants to view the conversation history b/w herself and Sachin, her requests only have to go to her shard. Her shard will contain all messages she has sent to Sachin and all messages Sachin has sent to her.
Sanjana ⇒ Sachin: this message must be stored in both Sanjana's shard & Sachin's shard. Not a fan-out.
It is created in the response - once the message has been sent (it's not part of the request)
What if we generate the message id on the client side? In that case, the message id can be part of the request.
Sharding my message id would mean that the messages will be distributed across servers - any message can go to any server (based on the hash of message id)
If I want to find all messages that Sanjana sent to Sachin - I will have to go to all shards, which is a fan-out
Consider a "Announcements" group that has 100,000 participants.
Sanjana sends "Hi" in that group.
If we shard by user_id, we can design in 2 ways
All messages that are sent & received within any group ("Announcements" for example) will be in a single shard.
Sanjana ⇒ "Hi" ⇒ Announcements
2 possible approaches:
You should only use it if you individual data items (1 row) is “small”: < 1kb, or a few kbs max. If your rows are usually > 10Kb, then you should probably not use a SQL database.
Note that your database will not complain even if you try to store 100MB row. It’s just stupid/bad-database-choice to store such large values in a SQL db row.
This does NOT mean that SQL can only handle 10kb of data. SQL can handle terabytes of data.. it’s just that the data must be in multiple rows and tables.. the individual rows should not be too large.
The HDD might be spinning at 7200 RPM.
7200 rpm ⇒ 7200/60 rps ⇒ 120 rps
time to complete 1 rotation ⇒ 1 / (120) s = 8ms
~10ms
10ms to move the spindle to correct track
bandwidth = 100MB / 18ms = 100MB / 0.018s = 5.5Gbps
Why 4kb? Because your disk is NOT byte addressable. Whenever you read/write, you always do it in chunks of 4kb.
Because random data will be on some different track
10ms to move the spindle to correct track
bandwidth = 4kb / 18ms = 222kbps
Sequential access is up to 10,000x faster than random access!
True irrespective of the storage technology (HDD, SSD, RAM, L3 cache, Brain)